﻿using System;
using System.IO;
using Ionic.Zip;
using System.Linq;
using System.Text;
using System.Globalization;
using System.IO.Compression;
using System.Collections.Generic;
using System.Collections.ObjectModel;

namespace NazTek.Extension.Clr4
{
    /// <summary>
    /// Extends FileInfo
    /// </summary>
    public static class FileInfoExtension
    {
        /// <summary>
        /// Compresses file wih <see cref="GZipStream"/>
        /// </summary>
        /// <param name="file">The file to compress</param>
        /// <param name="compressedFileName">Qualified name of the target compressed file.  Folder name is used as the filename and it's parent folder is used as path, when unspecified.</param>
        /// <returns>The compressed file</returns>
        /// <example><code>myFile.Compress(); //myFile is an instance of type FileInfo</code></example>
        public static CompressedFileInfo Compress(this FileInfo file, string compressedFileName = null)
        {
            if (!Parameter.IsValid(compressedFileName))
                compressedFileName = string.Format(CultureInfo.InvariantCulture, "{0}{1}", file.FullName, StringResource.Default.Cst_ZipFileExtension);
            using (ZipFile compressedFile = new ZipFile())
            {
                compressedFile.AddFile(file.FullName, "");
                compressedFile.Save(compressedFileName);
            }
            return new CompressedFileInfo(compressedFileName, file.Length);
        }
        /// <summary>
        /// Decompresses file with <see cref="GZipStream"/>
        /// </summary>
        /// <param name="file">Wraps the file to decompress</param>
        /// <param name="decompressFolder"></param>
        /// <param name="overrideExisting"></param>
        /// <returns>The decompressed file</returns>
        /// <example><code>myZipFile.Decompress(); //myZipFile is an instance of type FileInfo</code></example>
        public static DirectoryInfo Decompress(this FileInfo file, DirectoryInfo decompressFolder = null, bool overrideExisting = false)
        {
            // hydrate optional parameter value, decompressFolder
            if (!Parameter.IsValid<DirectoryInfo>(decompressFolder))
            {
                string curFile = file.FullName;
                decompressFolder = new DirectoryInfo(curFile.Remove(curFile.Length - file.Extension.Length));
            }

            // delete existing target
            Action<bool, Action> prepareTarget = (targetExists, deleteTarget) =>
            {
                if (targetExists)
                {
                    if (overrideExisting == true)
                        deleteTarget();
                    else
                        throw new CustomException(string.Format(CultureInfo.CurrentCulture, StringResource.Default.Msg_TargetAlreadyExists, decompressFolder));
                }
            };
            prepareTarget(decompressFolder.Exists, () => Directory.Delete(decompressFolder.FullName));
            prepareTarget(File.Exists(decompressFolder.FullName), () => File.Delete(decompressFolder.FullName));

            // unzip files
            using (ZipFile compressedFile = new ZipFile(file.FullName))
                compressedFile.ExtractAll(decompressFolder.FullName);

            return decompressFolder;
        }
        /// <summary>
        /// Splits a data file into multiple files
        /// </summary>
        /// <param name="file">The file to split</param>
        /// <param name="maxFileSize">Maximum size of the target files in bytes</param>
        /// <param name="targetDirectory">Folder to place the new files in, when unspecified, source file's folder is used</param>
        /// <param name="headerLineCount">Number of first lines from the source file to repeat in the target files</param>
        /// <returns>List of new files</returns>
        public static ReadOnlyCollection<FileInfo> Split(this FileInfo file, int maxFileSize, string targetDirectory = null, int headerLineCount = 0)
        {
            Parameter.Validate<FileInfo>("file", file);
            Parameter.Validate("maxFileSize", maxFileSize, 0);

            List<string> recs = new List<string>(File.ReadAllLines(file.FullName, Encoding.Default));
            string[] header = new string[] { };
            if (headerLineCount > 0)
            {
                List<string> headerList = new List<string>(headerLineCount);
                headerList.AddRange(recs.GetRange(0, headerLineCount));
                header = headerList.ToArray<string>();
                recs.RemoveRange(0, headerLineCount);
            }

            const int MAX_AVG_COUNTER = 10;
            const int MIN_REC_COUNT = 1;

            int recordCount = recs.Count;
            if ((recordCount > MAX_AVG_COUNTER))
                recordCount = MAX_AVG_COUNTER;
            int totalLength = recs.Take(recordCount).Sum((rec) => rec.Length);
            int avgLength = totalLength / recordCount;
            int maxRecCountPerFile = maxFileSize / avgLength;
            maxRecCountPerFile = maxRecCountPerFile < MIN_REC_COUNT ? MIN_REC_COUNT : maxRecCountPerFile;

            List<FileInfo> files = new List<FileInfo>();
            Func<string> getTargetFolder = () => targetDirectory[0] == @"\"[0] ? String.Format(CultureInfo.InvariantCulture, "{0}{1}", file.DirectoryName, targetDirectory) : targetDirectory;
            Action<int, int> insertRecs = (fileCounter, recCounter) =>
            {
                string targetFilename = String.Format(CultureInfo.InvariantCulture, @"{0}\{1}({2}){3}",
                    string.IsNullOrEmpty(targetDirectory) || targetDirectory.Trim().Length == 0 ? file.DirectoryName : getTargetFolder(),
                    file.Name.Replace(file.Extension, ""),
                    fileCounter,
                    file.Extension);
                FileInfo targetFile = new FileInfo(targetFilename);
                targetFile.Directory.EnsureExists();
                List<string> targetRecs = new List<string>(header);
                targetRecs.AddRange(recs.Take(recCounter));
                File.WriteAllLines(targetFilename, targetRecs.ToArray(), Encoding.Default);

                files.Add(targetFile);
            };

            int counter = 0;
            while (recs.Count > maxRecCountPerFile)
            {
                insertRecs(counter, maxRecCountPerFile);
                recs.RemoveRange(0, maxRecCountPerFile);
                counter++;
            }
            if (recs.Count > 0)
            {
                insertRecs(counter, recs.Count);
                recs.Clear();
            }
            return new ReadOnlyCollection<FileInfo>(files);
        }
        /// <summary>
        /// Renames a file.
        /// </summary>
        /// <param name="file">The file to rename.</param>
        /// <param name="to">The new name.</param>
        /// <param name="targetNameIncludesExtension">Adds the source file's extension to the end of the target filename, when false.</param>
        /// <param name="deleteExistingTarget">Deletes any pre-existing target file, when true.</param>
        public static void Rename(this FileInfo file, string to, bool targetNameIncludesExtension = false, bool deleteExistingTarget = false)
        {
            Parameter.Validate<FileInfo>("file", file);
            Parameter.Validate("to", to);

            to = targetNameIncludesExtension ? to : string.Format(CultureInfo.InvariantCulture, "{0}{1}", to, file.Extension);
            string fullTargetFileName = string.Format(CultureInfo.InvariantCulture, @"{0}\{1}", file.DirectoryName, to);
            if (File.Exists(fullTargetFileName) && deleteExistingTarget)
                File.Delete(fullTargetFileName);
            file.MoveTo(fullTargetFileName);
        }
        /// <summary>
        /// Compresses file wih <see cref="GZipStream"/>
        /// </summary>
        /// <param name="file">The file to compress</param>
        /// <param name="destinationZipFile">The stream to compress the file into</param>
        /// <param name="leaveOpen">Leaves the stream open for more files to be added to the compressed file</param>
        internal static void Compress(this FileInfo file, FileStream destinationZipFile, bool leaveOpen)
        {
            Parameter.Validate<FileInfo>("file", file);
            Parameter.Validate<FileStream>("destinationZipFile", destinationZipFile);

            // Get the stream of the source file.
            using (FileStream inFile = file.OpenRead())
                // Prevent compressing hidden and already compressed files.
                if ((File.GetAttributes(file.FullName) & FileAttributes.Hidden) != FileAttributes.Hidden & file.Extension != StringResource.Default.Cst_ZipFileExtension)
                    // Create the compressed file.
                    using (GZipStream Compress = new GZipStream(destinationZipFile, CompressionMode.Compress, leaveOpen))
                    {
                        // Copy the source file into the compression stream.
                        byte[] buffer = new byte[4096];
                        int numRead;
                        while ((numRead = inFile.Read(buffer, 0, buffer.Length)) != 0)
                            Compress.Write(buffer, 0, numRead);
                    }
        }
    }
}